Go中的类型

Author Avatar
子语 2018 - 01 - 17
  • 在其它设备中阅读本文章

Go是一种静态类型的语言。这意味着编译器需要在编译时知道程序中每个值的类型。这有助于减少潜在的内存异常和bug,并且使编译器有机会对代码进行一些性能优化,提高执行效率。

自定义类型

Go允许用户自定义类型,当用户声明一个新类型时,该声明就给编译器提供一个框架,告知必要的内存大小和表示信息Go中有两种声明类型的方式,,一种是使用关键字struct,用于创建一个结构类型。结构类型通过组合一系列固定且唯一的字段来声明。结构中的字段可以用Go内置的类型声明,也可以用自定义的类型声明。

package main

import "fmt"

// 定义一个user类型
type user struct {
	name string
	age int
	sex bool
}

func main() {

	// 声明user型变量
	var u user

	// 不赋值时,每个字段为其对应数据类型的默认值
	fmt.Println(u) // &#123"" 0 false&#125
	// 给结构中的字段赋值
	u.name = "Tom"
	u.age = 18
	u.sex = true

	// 给结构中的字段赋值,可以不按照结构中字段的顺序进行赋值
	lisa := user{
		name: "Lisa",
		age:21,
		sex: false,
	}

	// 给结构中的字段赋值,必须按照结构中字段的顺序进行赋值
	bill := user{
		"Bill", 
		8, 
		true
	}

	fmt.Println(u)
	fmt.Println(lisa)
	fmt.Println(bill)
}

使用自定义类型声明类型中的字段

package main

import "fmt"

// 定义一个user类型
type user struct {
   name string
   age int
   sex bool
}

// 使用自定义类型声明结构中的字段
type admin struct {
   person user
   level string
}

func main() {
   u := admin{
      person:user {
         name:"admin",
         age:18,
         sex:true,
      },
      level:"1",
   }

   fmt.Println(u) 

此外还可以基于已有的类型,声明新类型的类型。当需要一个可以用已有类型表示新的类型时,这个方式很好用。

package main

import "fmt"

type Duration int64

func main() {
   var dur Duration
   dur = int64(1000)
   // cannot use int64(1000) (type int64) as type Duration in assignment

   fmt.Println(dur) 
}

尽管 int64Duration的基础类型,但是Go不会认为它们是同一种类型。因此当将int64的值赋予dur时会报错。两种类型即使互相兼容,但不能互相赋值,编译器不会对不同类型的值做隐式转换。

方法

方法能给用户定义的类型添加新的行为。方法实际也是函数,只是在声明时,在关键字func和方法名之间增加了一个参数。

package main

import "fmt"

// 定义一个user类型
type user struct {
   name  string
   email string
}

type people struct {
   name  string
   email string
}

// 定义一个方法,(u user)表示只有类型为user的变量才可以接收该方法
func (u user) notify(){
   fmt.Printf("Sending User Email To %s<%s> \n", u.name, u.email)
}

func main() {
   bill := user{
   	"bill", 
   	"bill@email.com"
   }
   // 只有类型为user的变量才能调用notify()
   bill.notify()
   lisa := people{
   	"lisa", 
   	"lisa@email.com"
   }
   // 报错,type people has no field or method notify
   lisa.notify()

}

该参数用于定义方法的接收者,即定义调用方法的变量。Go中的接收者有两种类型:值接收者和指针接收者。值接受者声明的方法,在调用时,会用这个值的副本来执行。

package main

import "fmt"

// 定义一个user类型
type user struct {
   name  string
   email string
}

// 定义一个指针接收者方法
func (u *user) changeEmail(email string)  {
   u.email = email
}

// 定义一个值接收者方法
func (u user) changeName(name string)  {
   u.name = name
}

func main() {
   // user类型的值可以调用值接收者方法,也可以调用指针接收者方法
   bill := user{
   	"bill", 
   	"bill@email.com"
   }
   bill.changeName("lisa")
   bill.changeEmail("lisa@email.com")
   fmt.Print(bill) // {bill lisa@email.com}

   // user类型的值的指针可以调用指针接收者方法,也可以调用值接收者方法
   lisa := &user{"lisa", "lisa@email.com"}
   lisa.changeName("Lisa")
   lisa.changeEmail("Lisa@mail.com")
   fmt.Println(lisa) // {lisa Lisa@mail.com}
}

由结果我们可以看出,值接收者声明的方法,执行后不会改变调用该方法的变量本身的值。但指针接收者声明的方法,不论是普通变量还是指针变量调用后,都会改变自身的值。这是因为值接收者声明的方法,被调用后,接收到的是bill的副本,而指针接收者声明的方法被调用后接收到的是变量自身的实际值。指针变量能够调用值接收者方法,是因为Go调整了指针的值,指针被解引用为值,即等价于(*lisa).changeName("Lisa")。普通变量能够调用指针接收者方法,是因为Go先引用bill得到了一个指针,即等价于(&bill).changeEmail("lisa@email.com")

类型的本质

声明一个类型之后,声明一个该类型的方法之前,需要考虑以下几点:

  • 该类型的本质是什么
  • 给该类型增加或删除某个值,是要创建新值还是改变当前值。如果是创建新值,则使用值接收者声明方法;如果是修改当前值,则使用指针接收者声明方法。

基本类型

基本类型是由语言自身提供的一组类型,如数值类型、字符串类型、布尔类型。这些类型是本质是原始的类型,因此对这些值进行操作时,会创建新值。基于这个 理论,当这些类型的值传递给方法或函数时,应该传递一个对应值的副本。

package main

import (
   "fmt"
   "strings"
)

func main() {
   var s string = "Hello, World"
   // Trim删除s中的H、e、l、o、d字母
   fmt.Println(strings.Trim(s, "Helod")) // 生成了新的字符串 ", Wor"
   fmt.Println(s)  // s本身不变
}

字符串本质上是基本类型,所以在函数或方法内外传递时,传递的是字符串的副本。

引用类型

Go中的引用类型有:切片、映射、通道、接口和函数类型。当声明上述类型的变量时,创建的变量被称为header值。从技术细节来讲,字符串也是引用类型。每个引用类型的创建的header值,包含一个指向底层数据结构的指针,还包含一组独特的字段,用于管理底层数据结构。因为header值是为复制而设计的,所以永远不需要共享一个引用类型的值。header值里包含一个指针,因此通过复制来传递引用类型的值的副本,本质上就是共享底层数据结构。即引用类型的函数或方法内外传递时,依然使用的是数据副本。

结构类型

结构类型可以用于描述一组数据,这组数据的本质可以是原始的,也可以是非原始的,如果要决定在某些数据需要删除或添加结构类型的值时,该结构类型的值不应该被更改,那么需要遵守之前提到的基本类型和引用类型的规范。使用值接收者还是指针接收者,不应该由这个方法是否修改了值的本身决定,而是应该基于该类型的本质。其本质为原始,则使用值接收,非原始则使用指针接收。

接口

多态是指代码可以根据类型的具体实现采取不同行为的能力。如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。

实现

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。

方法集

方法集定义了接口的接收规则。

package main

import "fmt"

// 定义notifier接口,具有notify()行为
type notifier interface {
   notify()
}

// 定义user类型
type user struct {
   name  string
   email string
}

// 使用指针接收者实现notify()
func (u *user) notify() {
   fmt.Printf("Sending user email to  %s<%s>\n", u.name, u.email)
}

func main() {
   // 创建一个user类型的变量,并调用notify()
   u := user{
   	"Bill", 
   	"bill@email.com"
   }
   SendNotification(u)
   // cannot use u (type user) as type notifier in argument to SendNotification:
   // user does not implement notifier (notify method has pointer receiver)
}

// 接收一个实现了notifier接口的值
func SendNotification(n notifier)  {
   n.notify()
}

由报错的原因,我们可以看出notify()是指针接收者声明的。要想了解指针接收者来实现接口时为什么user类型的值无法实现该接口,就需要先了解方法集。方法集定义了一组关联到给定类型的值或指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针。

从接收者类型角度来看方法集,值接收者定义的方法可以接收值或指针,而指针接收者定义的方法只能接收指针。所以将代码改为下述形式后,就不会报错了:

func main() {
   // 创建一个user类型的变量,并调用notify()
   u := user{
   	"Bill", 
   	"bill@email.com"
   }
   SendNotification(&u)
}

多态

package main

import "fmt"

// 定义notifier接口,具有notify()行为
type notifier interface {
   notify()
}

// 定义user类型
type user struct {
   name  string
   email string
}

// user实现的notify()
func (u *user) notify() {
   fmt.Printf("Sending user email to  %s<%s>\n", u.name, u.email)
}

// 定义admin类型
type admin struct {
   name string
   age  int
}

// admin实现的notify()
func (a *admin) notify() {
   fmt.Printf("%s is %d years old.", a.name, a.age)
}

func main() {
   // 创建一个user类型的变量,并调用notify()
   u := user{
   	"Bill", 
   	"bill@email.com"
   }
   SendNotification(&u)

   // 创建一个admin类型的变量,并调用notify()
   a := admin{
   	"Lisa", 
   	18}
   SendNotification(&a)
}

// 接收一个实现了notifier接口的值
func SendNotification(n notifier)  {
   n.notify()
}

任意一个实体类型都能实现接口notifier,因此SendNotification可以针对实现接口的类型,实现具体的行为。这就是多态。

嵌入类型

Go语言允许用户扩展或者修改已有类型的行为。这个功能对代码复用很重要,在修改已有类型以符合新类型的时候很重要。这个功能是通过嵌入类型(type embedding)完成的。嵌入类型是将已有的类型直接声明在新结构类型中。被嵌入的类型被称为新的外部类型的内部类型。

通过嵌入类型,与内部类型相关的标识符会提升到外部类型上。这些被提升的标识符就像直接声明在外部类型里的标识符一样,也是外部类型的一部分。这样外部类型就组合了内部类型包含的所有属性,并且可以添加新的字段和方法。外部类也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符或者方法。

package main

import "fmt"

type user struct {
   name  string
   email string
}

func (u *user) notify() {
   fmt.Printf("Sending user email to %s<%s>\n", u.name, u.email)
}

type admin struct {
   user // 嵌入类型
   level string
}

func main() {
   ad := admin{
      user: user{
         name:  "john smith",
         email: "john@yohoo.com",
      },
      level: "super",
   }

   // 可通过内部类型调用内部类型的方法
   ad.user.notify()

   // 也可以直接使用外部类型直接调用方法
   ad.notify()
}

上述代码中将user嵌入admin类型中只需要在外部类型中声明要嵌入的类型名即可。对于外部类型而言,内部类型是存在的,所以即使外部类型可以使用内部类型的属性名来访问内部类型的值。也可以直接访问内部类型user来调用外部类型方法,也可以直接用外部类型的变量调用notify()

package main

import "fmt"

// 定义个接口,接口有notify()
type notifier interface {
   notify()
}

type user struct {
   name  string
   email string
}

// 通过user类型的值的指针调用notify()
func (u *user) notify() {
   fmt.Printf("Sending user email to %s<%s>\n", u.name, u.email)
}

type admin struct {
   user // 嵌入类型
   level string
}

func main() {
   ad := admin{
      user: user{
         name:  "john smith",
         email: "john@yohoo.com",
      },
      level: "super",
   }

   // admin类型的值的指针也可以传入
   sendNotification(&ad)
}

// 接收一个实现notifier接口的值
func sendNotification(n notifier) {
   n.notify()
}                     

由上述代码可知,如果嵌入类型实现了某接口,外部类型也相当于实现了该接口。如果外部类型并不需要使用内部类型对接口的实现方式,想定义属于自己的实现方式,应按照下述代码:

func (a *admin) notify() {
	fmt.Printf("Sending admin email to %s<%s>\n", a.name, a.email)
}

在上述代码添加由admin类型的值的指针调用的方法,此时再运行上述代码,调用的就不再是user类型的值的指针调用的方法。

func main() {
   ad := admin{
      user: user{
         name:  "john smith",
         email: "john@yohoo.com",
      },
      level: "super",
   }

   // admin类型的值的指针也可以传入
   sendNotification(&ad)  // Sending admin email to john smith<john@yohoo.com>
   sendNotification(&ad.user) // Sending user email to john smith<john@yohoo.com>
}

公开或未公开的标识符

定义一个counters/counters.go

package counters

// 未公开类型
type alertCounter int

此时在主程序中调用该类型

package main

import (
   "demo/counters"
   "fmt"
)

func main() {
   counter := counters.alertCounter(10)
   fmt.Println(counter)
   /*
   .\main.go:9:13: cannot refer to unexported name counters.alertCounter
   .\main.go:9:13: undefined: counters.alertCounter
    */

}

在Go中,当一个标识符以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见。当一个标识符以大写字母开头,其就是公开的,即包外可见。修改上述程序:

package counters

// 公开类型
type AlertCounter int

=======================================

package main

import (
   "demo/counters"
   "fmt"
)

func main() {
   counter := counters.AlertCounter(10)
   fmt.Println(counter) // 10
}

或者在不修改counter.goalertCounter的情况下,也可以使用以下方式调用未公开的类型:

package counters

// 未公开类型
type alertCounter int

// 创建并返回一个alertCounter类型的值
func New(value int) alertCounter  {
   return alertCounter(value)
}

===========================================
package main

import (
	"demo/counters"
	"fmt"
)

func main() {
	counter := counters.New(10)
	fmt.Println(counter) // 10
}

我们可以实现工厂函数,来返回一个未公开类型的值。Go语言习惯将工厂函数命名为New,它创建了一个未公开类型的值,并将其返回给调用者。该程序能通过编译,需要两个条件:第一、公开或未公开的标识符不是一个值;第二、短变量声明操作符,有能力捕获引用的类型,并创建一个未公开的类型的变量。永远不能显式创建一个未公开的类型的变量,不过短变量声明操作符可以这么做。

package entities

type User struct {
   Name string
   // 未公开字段
   email string
}

==================

package main

import (
	"demo/entities"
	"fmt"
)

func main() {
	u := entities.User{
		Name:  "Bill",
		email: "bill@email.com",
		// unknown field 'email' in struct literal of type entities.User
	}
	fmt.Println(u)
}

上述代码出错,是由于email字段首字母为小写,是不公开字段。下面看展示公开和未公开的内嵌类型是如何工作的。

package entities

type user struct {
   Name string
   Email string
}

type Admin struct {
   user // 嵌入的类型未公开
   Rights int
}

=====================

package main

import (
	"demo/entities"
	"fmt"
)

func main() {
	a := entities.Admin{
		Rights: 10,
	}

	a.Name = "Bill"
	a.Email = "bill@email.com"

	fmt.Println(a)
}

上述代码无法直接通过结构字面量的方式初始化该内部类型。但即便内部类型是未公开,内部类型里声明的字段依旧是公开的,。既然内部类型的标识符提升到了外部类型,这些公开字段也可以通过外部类型的字段的值来访问。

This blog is under a CC BY-NC-SA 3.0 Unported License
本文链接:http://yov.oschina.io/article/Go/Go Base/Go中的类型/